package org.lmj.dim.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.lmj.dim.po.ServerResponse;
import org.lmj.dim.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * Spring security的总配置类
 * 配置密码验证规则、拦截的url、登录接口地址、登录成功与失败后的处理器、各种异常处理器
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    // Spring会自动寻找同样类型的具体类注入，这里就是JwtUserDetailsServiceImpl了
    @Autowired
    DbUserInfoService userDetailsService;
    //登录成功处理类，如返回自定义jwt
    @Autowired
    AuthenSuccessGeneTokenHandler authenticationSuccessHandler;
    //登录失败处理类
    @Autowired
    AuthenFailTurnHandler authenticationFailHandler;
    //权限不足处理类
    @Autowired
    AcsDenyHandler acsDenyHandler;
    //其他异常处理类
    @Autowired
    AuthenException authenException;

    /**
     * 定义登录时用户校验规则
     * @param authenticationManagerBuilder
     * @throws Exception
     */
    @Autowired
    public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                // 设置UserDetailsService 获取user对象
                /**
                 * 将用户数据服务对象加到security中
                 *
                 * 定义从数据库取用户信息的逻辑=========
                 */
                .userDetailsService(this.userDetailsService)
                // 自定义密码验证方法========================
                .passwordEncoder(new PasswordEncoder() {

                    @Override
                    public String encode(CharSequence charSequence) {
                        //这个方法没用，但必须重写
                        return "";
                    }

                    //自定义密码验证方法,charSequence:用户输入的密码，s:我们查出来的数据库密码
                    @Override
                    public boolean matches(CharSequence jsonParameterUserPassword, String dbUserPassword) {
                       // String pass = MD5Util.string2MD5(charSequence.toString());
                        System.out.println("用户输入密码:" +
                                jsonParameterUserPassword.toString() + "与数据库相同？" +
                                SecurityUtils.matchesPassword(jsonParameterUserPassword.toString(),dbUserPassword));
                        //未经过加密的密码charSequence和已经过加密的密码s
                        return SecurityUtils.matchesPassword(jsonParameterUserPassword.toString(),dbUserPassword);
                      //  return s.equals(charSequence.toString());  这是md5的方式
                    }
                });

    }

    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                //指定登录的地址，用户登录请求必须是表单格式，application/json方式是接受不到参数的
                //这样写参数可以 http://localhost:8080/user/login?username=admin&password=123456
                 //// 这里必须要写formLogin()，不然原有的UsernamePasswordAuthenticationFilter不会出现，也就无法配置我们重新的UsernamePasswordAuthenticationFilter
                 .formLogin()
                .loginProcessingUrl("/user/login")
//                .failureForwardUrl("/test/error") //这个没效果
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailHandler)
                .and()
                // 由于使用的是JWT，我们这里不需要csrf
                .csrf().disable()
                // 基于token，所以不需要session
              //  .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                //.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // 允许对于网站静态资源的无授权访问
                .antMatchers(
                        HttpMethod.GET,
                        "/",
                        "/*.html",
                        "/favicon.ico",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js"
                ).permitAll()
                // 对于获取token的rest api要允许匿名访问
                .antMatchers("/test/**","/regist","/login","/json/login","/whoAmI").permitAll()
                .anyRequest()
                // 除上面外的所有请求全部需要鉴权认证
               .authenticated();
        // 禁用缓存
        httpSecurity.headers().cacheControl();
         // 用重写的Filter替换掉原有的UsernamePasswordAuthenticationFilter
        httpSecurity.addFilterAt(customAuthenticationFilter(),
                UsernamePasswordAuthenticationFilter.class);
/**
 * 加入自己的一些安全处理逻辑
 */
        // 添加JWT filter
        httpSecurity
                //.addFilterBefore(jwtTokenFilter, LogoutFilter.class)
                // 添加权限不足 filter
                //Spring Security 中的异常处理
//        我们一般都会在Spring Security 的 自定义配置类（ WebSecurityConfigurerAdapter ）中使用HttpSecurity 提供的 exceptionHandling() 方法用来提供异常处理。该方法构造出 ExceptionHandlingConfigurer 异常处理配置类。该配置类提供了两个实用接口：
//        AuthenticationEntryPoint 该类用来统一处理 AuthenticationException 异常
//        AccessDeniedHandler 该类用来统一处理 AccessDeniedException 异常

                .exceptionHandling()
                .accessDeniedHandler(acsDenyHandler)
                //其他异常处理类
                .authenticationEntryPoint(authenException);
    }

    @Bean
    CustomUnamePwdAuthenFilter customAuthenticationFilter() throws Exception {
        CustomUnamePwdAuthenFilter filter = new CustomUnamePwdAuthenFilter();
        filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
                resp.setContentType("application/json;charset=utf-8");
                PrintWriter out = resp.getWriter();
                ServerResponse<Object> result = ServerResponse.fail(401, "未登录");
                out.write(new ObjectMapper().writeValueAsString(result));
                out.flush();
                out.close();
            }
        });
        //这行代码很关键，它定义了拦截的路径，不写的话可能也会导致改拦截器失效，因为你重写了UsernamePasswordAuthenticationFilter
        //而原来的是拦截/login，以及是从表单获取
        //所以重写之后如果不写路径，就不会走到该拦截链
        filter.setFilterProcessesUrl("/json/login/");
        //这句很关键，重用WebSecurityConfigurerAdapter配置的AuthenticationManager，不然要自己组装AuthenticationManager
        filter.setAuthenticationManager(authenticationManagerBean());
        return filter;
    }
}